import { readFileSync, writeFileSync } from 'node:fs';
import { basename, extname, resolve } from 'node:path';

import { getOctokit } from '@actions/github';

import {
  githubBaseUrl,
  isGitea,
  owner,
  releaseAssetNamePattern,
  repo,
  updaterJsonPreferNsis,
} from './inputs';
import { uploadAssets } from './upload-release-assets';
import {
  createArtifact,
  deleteGiteaReleaseAsset,
  getAssetName,
  ghAssetName,
} from './utils';

import type { Artifact, TargetInfo } from './types';

type Platform = {
  signature: string;
  url: string;
};

type VersionContent = {
  version: string;
  notes: string;
  pub_date: string;
  platforms: {
    [key: string]: Platform;
  };
};

export async function uploadVersionJSON(
  version: string,
  notes: string,
  tagName: string,
  releaseId: number,
  artifacts: Artifact[],
  targetInfo: TargetInfo,
  unzippedSig: boolean,
) {
  if (process.env.GITHUB_TOKEN === undefined) {
    throw new Error('GITHUB_TOKEN is required');
  }

  const github = getOctokit(process.env.GITHUB_TOKEN, {
    baseUrl: githubBaseUrl,
  });

  const versionFilename = 'latest.json';
  const versionFile = resolve(process.cwd(), versionFilename);
  const versionContent: VersionContent = {
    version,
    notes,
    pub_date: new Date().toISOString(),
    platforms: {},
  };

  const assets = await github.rest.repos.listReleaseAssets({
    owner,
    repo,
    release_id: releaseId,
    per_page: 50,
  });
  const asset = assets.data.find((e) => e.name === versionFilename);

  if (asset) {
    if (isGitea) {
      const info = (
        await github.request(
          'GET /repos/{owner}/{repo}/releases/{release_id}/assets/{asset_id}',
          {
            owner,
            repo,
            release_id: releaseId,
            asset_id: asset.id,
          },
        )
      ).data as { browser_download_url: string };

      const data = (await github.request(`GET ${info.browser_download_url}`))
        .data as string;

      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      versionContent.platforms = JSON.parse(
        data,
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      ).platforms;
    } else {
      const assetData = (
        await github.request(
          `GET /repos/{owner}/{repo}/releases/assets/{asset_id}`,
          {
            owner: owner,
            repo: repo,
            release_id: releaseId,
            asset_id: asset.id,
            headers: {
              accept: 'application/octet-stream',
            },
          },
        )
      ).data as unknown as ArrayBuffer;

      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      versionContent.platforms = JSON.parse(
        Buffer.from(assetData).toString(),
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      ).platforms;
    }
  }

  const downloadUrls: {
    name: string;
    label: string | null;
    url: string;
  }[] = [];
  for (const data of assets.data) {
    downloadUrls.push({
      name: data.name,
      label: data.label,
      url: data.browser_download_url,
    });
  }

  // Assets matching artifacts generated by this action
  const filteredAssets = [];
  // Signature files may not be uploaded to the release so we collect them separately
  const signatureFiles = [];
  // We need to check for these so that we can non-destructively overwrite the relevant json entries with universal builds if needed
  let hasNativeArm;
  let hasNativeX64;

  for (const artifact of artifacts) {
    if (artifact.ext === '.app.tar.gz' && artifact.arch === 'universal') {
      const arm = { ...artifact, arch: 'aarch64' };
      const x64 = { ...artifact, arch: 'x86_64' };

      const armName = ghAssetName(arm, releaseAssetNamePattern);
      const armLabel = getAssetName(arm, releaseAssetNamePattern);
      const x64Name = ghAssetName(x64, releaseAssetNamePattern);
      const x64Label = getAssetName(x64, releaseAssetNamePattern);

      hasNativeArm = !!downloadUrls.find(
        (a) => a.label === armLabel || a.name === armName,
      );
      hasNativeX64 = !!downloadUrls.find(
        (a) => a.label === x64Label || a.name === x64Name,
      );
    }

    const assetLabel = getAssetName(artifact, releaseAssetNamePattern);
    const assetName = ghAssetName(artifact, releaseAssetNamePattern);
    const downloadUrl = downloadUrls.find(
      (a) => a.label === assetLabel || a.name === assetName,
    )?.url;

    if (artifact.ext.endsWith('.sig')) {
      signatureFiles.push({
        assetLabel,
        assetName,
        path: artifact.path,
        arch: artifact.arch,
        bundle: artifact.bundle,
      });
    } else if (downloadUrl) {
      filteredAssets.push({
        downloadUrl,
        assetLabel,
        assetName,
        path: artifact.path,
        arch: artifact.arch,
        bundle: artifact.bundle,
      });
    }
  }

  function signaturePriority(signaturePath: string) {
    if (
      (unzippedSig && signaturePath.endsWith('.AppImage.sig')) ||
      (!unzippedSig && signaturePath.endsWith('.AppImage.tar.gz.sig'))
    ) {
      return 100;
    }
    const priorities = updaterJsonPreferNsis
      ? unzippedSig
        ? ['.exe.sig', '.msi.sig']
        : ['.nsis.zip.sig', '.msi.zip.sig']
      : unzippedSig
        ? ['.msi.sig', '.exe.sig']
        : ['.msi.zip.sig', '.nsis.zip.sig'];
    for (const [index, extension] of priorities.entries()) {
      if (signaturePath.endsWith(extension)) {
        return 100 - index;
      }
    }
    return 0;
  }
  signatureFiles.sort((a, b) => {
    return signaturePriority(b.path) - signaturePriority(a.path);
  });

  if (!signatureFiles[0]) {
    console.warn(
      'Signature not found for the updater JSON. Skipping upload...',
    );
    return;
  }

  for (const [idx, signatureFile] of signatureFiles.entries()) {
    const updaterFileLabel = basename(
      signatureFile.assetLabel,
      extname(signatureFile.assetLabel),
    );
    const updaterFileName = basename(
      signatureFile.assetName,
      extname(signatureFile.assetName),
    );
    let updaterFileDownloadUrl = filteredAssets.find(
      (a) =>
        a.assetLabel === updaterFileLabel || a.assetName === updaterFileName,
    )?.downloadUrl;

    if (!updaterFileDownloadUrl) {
      console.warn(
        `Updater asset belonging to signature file "${signatureFile.assetName}" not found.`,
      );
      continue;
    }

    // Untagged release downloads won't work after the release was published
    updaterFileDownloadUrl = updaterFileDownloadUrl.replace(
      /\/download\/(untagged-[^/]+)\//,
      tagName
        ? `/download/${encodeURIComponent(tagName)}/`
        : '/latest/download/',
    );

    let os = targetInfo.platform as string;
    if (os === 'macos') {
      os = 'darwin';
    }

    let arch = signatureFile.arch;
    arch =
      arch === 'amd64' || arch === 'x86_64' || arch === 'x64'
        ? 'x86_64'
        : arch === 'x86' || arch === 'i386'
          ? 'i686'
          : arch === 'arm'
            ? 'armv7'
            : arch === 'arm64'
              ? 'aarch64'
              : arch;

    // This is our primary updater type we use for `{os}-{arch}`
    if (idx === 0) {
      if (os === 'darwin' && arch === 'universal') {
        // Don't overwrite native builds unless outdated.
        // hasNativeArm/x64 can be false while other jobs are overwriting artifacts,
        // but we should still always end up with a working latest.json file.
        if (!versionContent.platforms['darwin-aarch64'] || !hasNativeArm) {
          (versionContent.platforms['darwin-aarch64'] as unknown) = {
            signature: readFileSync(signatureFile.path).toString(),
            url: updaterFileDownloadUrl,
          };
        }
        if (!versionContent.platforms['darwin-x86_64'] || !hasNativeX64) {
          (versionContent.platforms['darwin-x86_64'] as unknown) = {
            signature: readFileSync(signatureFile.path).toString(),
            url: updaterFileDownloadUrl,
          };
        }
      }
      (versionContent.platforms[`${os}-${arch}`] as unknown) = {
        signature: readFileSync(signatureFile.path).toString(),
        url: updaterFileDownloadUrl,
      };
    }

    // This is for the new `{os}-{arch}-{installer}` format
    if (os === 'darwin' && arch === 'universal') {
      // Don't overwrite native builds unless outdated.
      // hasNativeArm/x64 can be false while other jobs are overwriting artifacts,
      // but we should still always end up with a working latest.json file.
      if (!versionContent.platforms['darwin-aarch64-app'] || !hasNativeArm) {
        (versionContent.platforms['darwin-aarch64-app'] as unknown) = {
          signature: readFileSync(signatureFile.path).toString(),
          url: updaterFileDownloadUrl,
        };
      }
      if (!versionContent.platforms['darwin-x86_64-app'] || !hasNativeX64) {
        (versionContent.platforms['darwin-x86_64-app'] as unknown) = {
          signature: readFileSync(signatureFile.path).toString(),
          url: updaterFileDownloadUrl,
        };
      }
    }
    (versionContent.platforms[
      `${os}-${arch}-${signatureFile.bundle}`
    ] as unknown) = {
      signature: readFileSync(signatureFile.path).toString(),
      url: updaterFileDownloadUrl,
    };
  }

  writeFileSync(versionFile, JSON.stringify(versionContent, null, 2));

  if (asset) {
    if (isGitea) {
      await deleteGiteaReleaseAsset(github, releaseId, asset.id);
    } else {
      // https://docs.github.com/en/rest/releases/assets#update-a-release-asset
      await github.rest.repos.deleteReleaseAsset({
        owner,
        repo,
        release_id: releaseId,
        asset_id: asset.id,
      });
    }
  }

  const artifact = createArtifact({
    path: versionFile,
    name: versionFilename,
    platform: targetInfo.platform,
    arch: '',
    bundle: '',
    version,
  });

  await uploadAssets(
    releaseId,
    [artifact],
    // The whole step will be retried where `uploadVersionJSON` is called.
    // Just in case it's a quick http hickup we retry it once here as well.
    1,
  );
}
